diff options
| author | sergio <33748103+colado@users.noreply.github.com> | 2025-12-29 23:45:36 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-29 21:45:36 +0000 |
| commit | 30fa06feac15609fe99657e02d171e024e825322 (patch) | |
| tree | 0992aaa3a1fd3f82f38d9fe0f6d84aa990feedfc /apps/mobile/app/dashboard/lists/[slug] | |
| parent | 5537fe85ed65444359bfd066707760d6395fc7a4 (diff) | |
| download | karakeep-30fa06feac15609fe99657e02d171e024e825322.tar.zst | |
feat(mobile): create new list edit screen (#2310)
* feat(mobile): create new edit screen and path
* refactor(mobile): use correct import for back navigation
* refactor(mobile): remove set state for list type
* feat(mobile): handle loading state
* feat(mobile): add error handling
* feat(mobile): add local validation for empty list name
* refactor(mobile): use correct param name in edit path
* feat(mobile): handle all pending state cases
* refactor(mobile): remove unnecessary return
* refactor(mobile): move type validation to top of the file
* refactor(mobile): revert validation order
* refactor(mobile): clean up submit values
* fix(mobile): fix button views
Diffstat (limited to 'apps/mobile/app/dashboard/lists/[slug]')
| -rw-r--r-- | apps/mobile/app/dashboard/lists/[slug]/edit.tsx | 152 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/lists/[slug]/index.tsx | 150 |
2 files changed, 302 insertions, 0 deletions
diff --git a/apps/mobile/app/dashboard/lists/[slug]/edit.tsx b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx new file mode 100644 index 00000000..6ccc2f26 --- /dev/null +++ b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from "react"; +import { View } from "react-native"; +import { router, useLocalSearchParams } from "expo-router"; +import { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; +import { useToast } from "@/components/ui/Toast"; +import { api } from "@/lib/trpc"; + +import { useEditBookmarkList } from "@karakeep/shared-react/hooks/lists"; + +const EditListPage = () => { + const { slug: listId } = useLocalSearchParams<{ slug?: string | string[] }>(); + const [text, setText] = useState(""); + const [query, setQuery] = useState(""); + const { toast } = useToast(); + const { mutate, isPending: editIsPending } = useEditBookmarkList({ + onSuccess: () => { + dismiss(); + }, + onError: (error) => { + // Extract error message from the error object + let errorMessage = "Something went wrong"; + if (error.data?.zodError) { + errorMessage = Object.values(error.data.zodError.fieldErrors) + .flat() + .join("\n"); + } else if (error.message) { + errorMessage = error.message; + } + toast({ + message: errorMessage, + variant: "destructive", + }); + }, + }); + + if (typeof listId !== "string") { + throw new Error("Unexpected param type"); + } + + const { data: list, isLoading: fetchIsPending } = api.lists.get.useQuery({ + listId, + }); + + const dismiss = () => { + router.back(); + }; + + useEffect(() => { + if (!list) return; + setText(list.name ?? ""); + setQuery(list.query ?? ""); + }, [list?.id, list?.query, list?.name]); + + const onSubmit = () => { + if (!text.trim()) { + toast({ message: "List name can't be empty", variant: "destructive" }); + return; + } + + if (list?.type === "smart" && !query.trim()) { + toast({ + message: "Smart lists must have a search query", + variant: "destructive", + }); + return; + } + + mutate({ + listId, + name: text.trim(), + query: list?.type === "smart" ? query.trim() : undefined, + }); + }; + + const isPending = fetchIsPending || editIsPending; + + return ( + <CustomSafeAreaView> + {isPending ? ( + <FullPageSpinner /> + ) : ( + <View className="gap-3 px-4"> + {/* List Type Info - not editable */} + <View className="gap-2"> + <Text className="text-sm text-muted-foreground">List Type</Text> + <View className="flex flex-row gap-2"> + <View className="flex-1"> + <Button + variant={list?.type === "manual" ? "primary" : "secondary"} + disabled + > + <Text>Manual</Text> + </Button> + </View> + <View className="flex-1"> + <Button + variant={list?.type === "smart" ? "primary" : "secondary"} + disabled + > + <Text>Smart</Text> + </Button> + </View> + </View> + </View> + + {/* List Name */} + <View className="flex flex-row items-center gap-1"> + <Text className="shrink p-2">{list?.icon || "🚀"}</Text> + <Input + className="flex-1 bg-card" + onChangeText={setText} + value={text} + placeholder="List Name" + autoFocus + autoCapitalize={"none"} + /> + </View> + + {/* Smart List Query Input */} + {list?.type === "smart" && ( + <View className="gap-2"> + <Text className="text-sm text-muted-foreground"> + Search Query + </Text> + <Input + className="bg-card" + onChangeText={setQuery} + value={query} + placeholder="e.g., #important OR list:work" + autoCapitalize={"none"} + /> + <Text className="text-xs italic text-muted-foreground"> + Smart lists automatically show bookmarks matching your search + query + </Text> + </View> + )} + + <Button disabled={isPending} onPress={onSubmit}> + <Text>Save</Text> + </Button> + </View> + )} + </CustomSafeAreaView> + ); +}; + +export default EditListPage; diff --git a/apps/mobile/app/dashboard/lists/[slug]/index.tsx b/apps/mobile/app/dashboard/lists/[slug]/index.tsx new file mode 100644 index 00000000..11379588 --- /dev/null +++ b/apps/mobile/app/dashboard/lists/[slug]/index.tsx @@ -0,0 +1,150 @@ +import { Alert, Platform, View } from "react-native"; +import * as Haptics from "expo-haptics"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; +import FullPageError from "@/components/FullPageError"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { api } from "@/lib/trpc"; +import { MenuView } from "@react-native-menu/menu"; +import { Ellipsis } from "lucide-react-native"; + +import { ZBookmarkList } from "@karakeep/shared/types/lists"; + +export default function ListView() { + const { slug } = useLocalSearchParams(); + if (typeof slug !== "string") { + throw new Error("Unexpected param type"); + } + const { + data: list, + error, + refetch, + } = api.lists.get.useQuery({ listId: slug }); + + return ( + <CustomSafeAreaView> + <Stack.Screen + options={{ + headerTitle: list ? `${list.icon} ${list.name}` : "", + headerBackTitle: "Back", + headerLargeTitle: true, + headerRight: () => ( + <ListActionsMenu listId={slug} role={list?.userRole ?? "viewer"} /> + ), + }} + /> + {error ? ( + <FullPageError error={error.message} onRetry={() => refetch()} /> + ) : list ? ( + <View> + <UpdatingBookmarkList + query={{ + listId: list.id, + }} + /> + </View> + ) : ( + <FullPageSpinner /> + )} + </CustomSafeAreaView> + ); +} + +function ListActionsMenu({ + listId, + role, +}: { + listId: string; + role: ZBookmarkList["userRole"]; +}) { + const { mutate: deleteList } = api.lists.delete.useMutation({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }); + + const { mutate: leaveList } = api.lists.leaveList.useMutation({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }); + + const handleDelete = () => { + Alert.alert("Delete List", "Are you sure you want to delete this list?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + onPress: () => { + deleteList({ listId }); + }, + style: "destructive", + }, + ]); + }; + + const handleLeave = () => { + Alert.alert("Leave List", "Are you sure you want to leave this list?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Leave", + onPress: () => { + leaveList({ listId }); + }, + style: "destructive", + }, + ]); + }; + + const handleEdit = () => { + router.push({ + pathname: "/dashboard/lists/[slug]/edit", + params: { slug: listId }, + }); + }; + + return ( + <MenuView + actions={[ + { + id: "edit", + title: "Edit List", + attributes: { + hidden: role !== "owner", + }, + }, + { + id: "delete", + title: "Delete List", + attributes: { + destructive: true, + hidden: role !== "owner", + }, + image: Platform.select({ + ios: "trash", + }), + }, + { + id: "leave", + title: "Leave List", + attributes: { + destructive: true, + hidden: role === "owner", + }, + }, + ]} + onPressAction={({ nativeEvent }) => { + if (nativeEvent.event === "delete") { + handleDelete(); + } else if (nativeEvent.event === "leave") { + handleLeave(); + } else if (nativeEvent.event === "edit") { + handleEdit(); + } + }} + shouldOpenOnLongPress={false} + > + <Ellipsis onPress={() => Haptics.selectionAsync()} color="gray" /> + </MenuView> + ); +} |
